package com.ctshk.common.log.aspect;

import cn.hutool.core.date.DateTime;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ctshk.common.annotations.OperationLogAnnotation;
import com.ctshk.common.constant.RedisConstants;
import com.ctshk.common.enums.OperationLogType;
import com.ctshk.common.enums.SystemError;
import com.ctshk.common.exception.BusinessException;
import com.ctshk.common.log.entity.LoginLog;
import com.ctshk.common.log.entity.OperationLog;
import com.ctshk.common.log.mapper.LoginLogMapper;
import com.ctshk.common.log.mapper.OperationLogMapper;
import com.ctshk.common.log.utils.IpUtil;
import com.ctshk.common.model.TokenUser;
import com.ctshk.common.utils.RSAUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.util.Strings;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * @description 操作日志切面处理类
 * @author 谢诗宏
 * @date 2020/12/11
 */
@Slf4j
@Aspect
@Component
public class OperationLogAspect {

    @Resource
    private OperationLogMapper operationLogMapper;


    @Resource
    public void setOperationLogMapper(OperationLogMapper operationLogMapper) {
        this.operationLogMapper = operationLogMapper;
    }

    @Resource
    private LoginLogMapper loginLogMapper;

    @Resource
    public void setLoginLogMapper(LoginLogMapper loginLogMapper) {
        this.loginLogMapper = loginLogMapper;
    }


    /**
     * 设置操作日志切入点   在注解的位置切入代码
     */
    @Pointcut("@annotation(com.ctshk.common.annotations.OperationLogAnnotation)")
    public void operLogPoinCut() {
    }

    /**
     * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
     */
    @Pointcut("execution(* com.ctshk..*.controller..*.*(..))")
    public void operExceptionLogPoinCut() {
    }

    /**
     * 在切点之前织入
     * @param joinPoint
     * @throws Throwable
     */
    @Before("operLogPoinCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 打印请求相关参数
        log.info("========================================== AOP Start ==========================================");
    }

    /**
     * 在切点之后织入
     * @throws Throwable
     */
    @After("operLogPoinCut()")
    public void doAfter() throws Throwable {
        // 接口结束后换行，方便分割查看
        log.info("=========================================== AOP End ===========================================");
    }

    /**
     * 记录操作日志,正常返回通知,连接点正常执行完成后执行,如果连接点抛出异常则不会执行
     * @param joinPoint 方法的执行点
     * @param result  方法返回值
     * @throws Throwable
     */
    @AfterReturning(returning = "result", value = "operLogPoinCut()")
    public void saveOperLog(JoinPoint joinPoint, Object result) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        try {
            // 将返回值转换成JSON对象
            JSONObject resultJson = (JSONObject) JSON.toJSON(result);

            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();

            OperationLog operationLog = new OperationLog();
            // 获取操作
            OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
            if (annotation != null) {
                operationLog.setModel(annotation.operModul().getCode());
                operationLog.setTitle(annotation.operTitle());
                operationLog.setType(annotation.operType().getCode());
                String details = annotation.operDesc();
                if (!StringUtils.isEmpty(details) && details.contains("#")) {
                    details = parseDetails(details, method, joinPoint.getArgs());
                }
                operationLog.setDescription(details);
            }
            Long userId = 0L;
            /**
             * 判断登录方法,单独加入登录日志中
             */
            if (annotation.operType().getCode().equals(OperationLogType.LOGIN.getCode())) {
                userId = resultJson.getJSONObject("data").getLong("id");
                // 参数名(参数名和参数值的位置一样)
                // String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
                // 参数值
                Object[] args = joinPoint.getArgs();
                // 获取登录类型
                Map<String, String> loginReq = getParams(String.valueOf(args[0]));
                // 获取系统相关信息
                UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));

                LoginLog loginLog = new LoginLog();
                loginLog.setUserId(userId);
                loginLog.setIp(IpUtil.getIpAddr(request));
                loginLog.setType(loginReq.get("type"));
                loginLog.setResult(resultJson.getString("msg"));
                loginLog.setBrowser(String.valueOf(userAgent.getBrowser()));
                loginLog.setOs(String.valueOf(userAgent.getOs()));
                loginLog.setGmtCreate(LocalDateTime.now());
                loginLogMapper.insert(loginLog);
            } else {
                /**
                 * 获取当前用户
                 */
                String token = request.getHeader(RedisConstants.JWT_TOKEN_HEADER);
                if (StringUtils.isBlank(token)) {
                    throw new BusinessException(SystemError.USER_1000);
                }

                TokenUser tokenUser = new TokenUser();
                try {
                    token = token.replace(RedisConstants.JWT_TOKEN_PREFIX, Strings.EMPTY);
                    String tokenDecrypt = RSAUtil.decryptStr(token);
                    tokenUser = JSONObject.parseObject(tokenDecrypt, TokenUser.class);
                } catch (Exception e) {
                    throw new BusinessException(SystemError.USER_1015);
                }
                userId = tokenUser.getId();
            }

            Long recordId = null;
            if (annotation.operType().getCode().equals(OperationLogType.ADD.getCode())
                    || annotation.operType().getCode().equals(OperationLogType.UPDATE.getCode())
                    || annotation.operType().getCode().equals(OperationLogType.DELETE.getCode())) {
                // 所有新增编辑删除操作都记录id
                try {
                    // 一般情况新增编辑删除操作成功后直接返回对应业务主键id
                    recordId = resultJson.getLong("data");
                } catch (Exception e) {
                    // 特殊情况返回的是dto对象
                    recordId = resultJson.getJSONObject("data").getLong("id");
                }
            }

            // 操作用户
            operationLog.setUserId(userId);
            // 操作IP
            operationLog.setIp(IpUtil.getIpAddr(request));
            // 记录id
            operationLog.setRecordId(recordId == null ? 0 : recordId);
            // 返回值信息
            operationLog.setResult(resultJson.getString("msg"));
            operationLog.setGmtCreate(LocalDateTime.now());
            // 保存日志
            operationLogMapper.insert(operationLog);

            log.info("请求成功日志：[{}]", operationLog);
        } catch (Exception e) {
            // e.printStackTrace();
            log.info("操作日志写入失败：{}", e.getMessage());
        }
    }

    /**
     * 异常返回通知，用于拦截异常日志信息 连接点抛出异常后执行
     * @param joinPoint 切入点
     * @param e         异常信息
     */
    @AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);

        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName;
            // 请求的参数
            Map<String, String> rtnMap = converMap(request.getParameterMap());
            // 将参数所在的数组转换成json
            String params = JSON.toJSONString(rtnMap);

            log.info("请求错误日志：URI[{}], 方法名[{}], 请求参数[{}], userId[{}], e[{}]", request.getRequestURI(), methodName, params, 1, stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));
        } catch (Exception e2) {
            e2.printStackTrace();
        }

    }

    /**
     * 转换request 请求参数
     *
     * @param paramMap request获取的参数数组
     */
    public Map<String, String> converMap(Map<String, String[]> paramMap) {
        Map<String, String> rtnMap = new HashMap<>();
        for (String key : paramMap.keySet()) {
            rtnMap.put(key, paramMap.get(key)[0]);
        }
        return rtnMap;
    }

    /**
     * 转换异常信息为字符串
     *
     * @param exceptionName    异常名称
     * @param exceptionMessage 异常信息
     * @param elements         堆栈信息
     */
    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuffer strbuff = new StringBuffer();
        for (StackTraceElement stet : elements) {
            strbuff.append(stet + "\n");
        }
        String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
        return message;
    }

    /**
     * 获取POST方式的request 请求参数
     * @param args
     * @return
     */
    public Map<String, String> getParams(String args) {
        String s1 = args.replace("LoginReq(","").replace(")","").replace(" ","");
        Map<String, String> map = new HashMap<String, String>();
        if (StringUtils.isNotBlank(s1)) {
            String[] parts = s1.split(",");
            for (int i = 0; i < parts.length; i++) {
                try{
                    String[] part = parts[i].split("=");
                    map.put(part[0], part[1]);
                }catch (IndexOutOfBoundsException e){
                    //不做处理
                }
            }
        }

        return map;
    }

    /**
     * 解析Spring EL表达式
     * @param details
     * @param method
     * @param args
     * @return
     */
    private String parseDetails(String details, Method method, Object[] args) {
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] parameterNames = discoverer.getParameterNames(method);
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        return parser.parseExpression(details).getValue(context, String.class);
    }
}